/**
	@file
	FAT16 file system functions (read/write/open/close files etc.)
	@todo Dynamically allocate arrays
	@author Konstantin Kastanov
*/
#include "headers/fat16.h"
#include "headers/partitioning.h"
#include "../tools/headers/common.h"
#include "../api/headers/errors.h"
#include "../api/headers/string.h"
#include "../api/headers/ctype.h"
#include "../memory/headers/kheap.h"
#include "../api/headers/io.h"

#define MIN(a, b) a<b?a:b

#define FILE_ENTRY_SIZE 32
#define DRIVE_DELIM ':'
#define PATH_DELIM '/'
#define FNAME_MAX_LNGT 8
#define FEXTENSION_MAX_LNGT 3

static u8int* rootDirCopy = 0; //Should be allocated dynamically
static u8int copiedPartition = 0; //Temp. Shows which mounted partition is copied (rootDirCopy, fatCopy)
static u16int* fatCopy=0; //Should be allocated dynamically

static BOOL nameCmp(u8int *dirEntry, u8int *dirName, u8int dirNameLngt, u8int dirEntryLngt);
static void copyFatAndRootDir(s8int partitionName, u8int fatNumber);

/**
	Reads boot block information from a specified sector. Sector should be already
	read to RAM and passed as pointer.

	@param[in] buff Pointer to the data read from sector
	@param[out] res Array to a structure where function places results (structure contains arrays,
					so it's impossible to send it as return value)
*/
void readBootBlock(u8int *buff, BootBlock *res)
{
	//BootBlock res;
	u8int i;
	u16int smallSize = 0;
	u32int largeSize = 0;

	for(i = 0; i < 8; i++)
	{
		res->description[i] = buff[i + 0x03];
	}
	res->description[8] = 0; //Zero terminated string

	res->bytesPerBlock = (buff[0x0c] << 8) | buff[0x0b];
	res->blocksPerAllocUnit = buff[0x0d];
	res->reservedBlocks = (buff[0x0f] << 8) | buff[0x0e];
	res->numberOfFats = buff[0x10];
	res->rootDirEntriesN = (buff[0x12] << 8) | buff[0x11];

	smallSize = (buff[0x14] << 8) | buff[0x13];
	largeSize = (u32int)((buff[0x23] << 24) | (buff[0x22] << 16) | (buff[0x21] << 8) | buff[0x20]);
	res->totalBlocks = (smallSize > largeSize)?smallSize:largeSize;
	
	res->mediaDescriptor = buff[0x15];
	res->blocksPerFat = (buff[0x17] << 8) | buff[0x16];
	res->volumeSerialNumber = (u32int)((buff[0x2A] << 24) | (buff[0x29] << 16) | (buff[0x28] << 8) | buff[0x27]);
	
	for(i = 0; i < 4; i++)
	{
		res->volumeLabel[i] = buff[0x2b + i];
	}
	res->volumeLabel[4] = 0;

	for(i = 0; i < 8; i++)
	{
		res->fileSystemId[i] = buff[0x36 + i];
	}
	res->fileSystemId[8] = 0;

	res->rootDirStartSector = res->reservedBlocks + res->blocksPerFat * res->numberOfFats;
	res->rootDirSize = res->rootDirEntriesN * FILE_ENTRY_SIZE / res->bytesPerBlock;

	return;
}


void getFileList(s8int *path, s8int *res)
{
	FileDescriptor file;
	u32int currentCluster;
	u32int currentSector;
	Partition* partInfo;
	u8int* buff;
	BOOL root = false;
	BOOL end = false;
	u32int entriesN;
	u32int cursor = 0;
	u32int i = 0, t = 0;
	FileDescriptor temp;

	buff = (u8int*)kmalloc(16385);
	getDirectoryFromPath(path, &file, true);

	if(getLastError() != NO_ERROR)
		return;
	
	if(!file.isDirectory && file.startingCluster)
	{
		setLastError(ERR_NOT_DIR);
		return;
	}

	partInfo = getMountedPartitionInfo((s8int)tolower(path[0]));

	currentCluster = file.startingCluster;
	

	if(currentCluster == 0)
	{
		root = true;
		entriesN = partInfo->bootBlock.rootDirEntriesN;
		//buff = (u8int*)rootDirCopy;
	}
	else
	{
		root = false;
		currentSector = clusterToSector(currentCluster, &(partInfo->bootBlock));
		entriesN = partInfo->bootBlock.blocksPerAllocUnit * partInfo->bootBlock.bytesPerBlock / FILE_ENTRY_SIZE;
	}

	while(currentCluster != 0xFFFF)
	{
		if(!root)
		{
			partRead((s8int)tolower(path[0]), buff, currentSector, partInfo->bootBlock.blocksPerAllocUnit);
			if(getLastError() != NO_ERROR)
				return;
		}

		for(i = 0; i < entriesN; i++)
		{
			fillFileDescriptor((root?rootDirCopy:buff) + i*32, &temp, (s8int)tolower(path[0]));
			if(temp.name[0] != 0 && temp.name[0] != (s8int)DELETED && temp.name[0] != '.' && !temp.isSystem) //Determine if the entry is a valid directory/file descriptor
			{
				end = false;
				for(t = 0; t < 8; t++)
				{
					if((temp.name[t]) && !end)
						res[cursor++] = temp.name[t];
					else
					{
						res[cursor++] = ' ';
						end = true;
					}
				}
				res[cursor++] = '.';
				end = false;
				for(t = 0; t < 3; t++)
				{
					if((temp.extension[t]) && !end)
						res[cursor++] = temp.extension[t];
					else
					{
						res[cursor++] = ' ';
						end = true;
					}
				}
				res[cursor++] = ' ';
				if(temp.isDirectory)
					res[cursor++] = 'd';
				else
					res[cursor++] = 'f';
				res[cursor++] = '\n';
			}
				
		}

		if(root)
			currentCluster = 0xFFFF; //To exit loop
		else
			currentCluster = fatCopy[currentCluster];

	}
	res[cursor] = 0;
	kfree(buff);
	return;
}

static void copyFatAndRootDir(s8int partitionName, u8int fatNumber)
{
	Partition *partInfo;
	u32int fatAddress;
	u32int i;
	if(!rootDirCopy)
	{
		rootDirCopy = (u8int*)kmalloc(20000);
	}
	if(!fatCopy)
	{
		fatCopy = (u16int*)kmalloc(20000 * sizeof(u16int));
	}
	copiedPartition = 0;
	partInfo = getMountedPartitionInfo(partitionName);
	if(getLastError() != NO_ERROR)
		return;

	if(fatNumber > partInfo->bootBlock.numberOfFats)
	{
		setLastError(ERR_INVALID_ARG);
		return;
	}

	fatAddress = partInfo->bootBlock.reservedBlocks + (fatNumber - 1) * partInfo->bootBlock.blocksPerFat;
	
	i = partRead(partitionName, (u8int *) fatCopy, fatAddress, partInfo->bootBlock.blocksPerFat);
	

	if(getLastError() != NO_ERROR)
	{
		return;
	}

	if(i != partInfo->bootBlock.blocksPerFat)
	{
		setLastError(ERR_IO);
		return;
	}


	//Copy root directory
	i = partRead(partitionName, rootDirCopy, partInfo->bootBlock.rootDirStartSector, partInfo->bootBlock.rootDirSize);
// 	int a;
// 	printf("Root dir:\n");
// 	for(a = 0; a < 100; a++)
// 		printf("0x%H  ", rootDirCopy[a]);
// 	printf("\n");
// 	for(;;);

	if(getLastError() != NO_ERROR)
	{
		return;
	}

	if(i != partInfo->bootBlock.rootDirSize)
	{
		setLastError(ERR_IO);
		return;
	}

	copiedPartition = partitionName;
}

/**
	Finds file/folder by path and returns its starting cluster number.
*/
u32int getDirectoryFromPath(s8int *path, FileDescriptor* resFileInfo, BOOL ignoreVolLabel)
{
	s8int partName = -1; //Partition letter/number
	u32int pathLngt = 0, end = 0, currentDir = 0;
	s32int cursor = 1; //Position of drive delimiter (colon) - first thing to check
	Partition *partInfo;
	s8int *temp;
	s8int nameSize = 0, extensionSize = 0;
	BOOL last = false; //Indicates that we reached the end of the path
	BOOL found = false;
	BOOL rootDir = true;
	BOOL inRoot = true;
	s8int resDirEntry;
	u8int* buff;
	strToLower(path); //Path is not case dependent
	
#ifdef DEBUG
printf("Entered getDirFromPath\n");
#endif
	pathLngt = strlen(path);
	buff = (u8int*)kmalloc(16385);
#ifdef DEBUG
	printf("Allocated buffer memory\n");
#endif

	//Check if found
	if((path[cursor] != DRIVE_DELIM) || (path[cursor + 1] != PATH_DELIM)) //Check colon after drive letter and slash after semicolon
	{
		setLastError(ERR_BAD_ADDRESS);
		return 0;
	}

	partName = path[0]; //Get drive letter

	if((partName < 'a') || (partName > ('a' + MAX_PARTITION_NUMBER - 1))) //Validate number
	{
		setLastError(ERR_PARTITION_NOT_FOUND);
		return 0;
	}

	strToUpper(path);

	//Get partition info
	partInfo = getMountedPartitionInfo(partName);
	if(getLastError() != NO_ERROR)
		return 0;
	if(partInfo->size == 0)
	{
		setLastError(ERR_PARTITION_NOT_FOUND);
		return 0;
	}


	cursor = 3; //Move cursor to the first file/directory in the path (after root)

	if(copiedPartition != partName)
	{
		copyFatAndRootDir(partName, 1);
	}

	if(getLastError() != NO_ERROR)
	{
		return 0;
	}


	currentDir = 0;
	rootDir = true;

	while(!last && (path[cursor] != 0))
	{
		found = false;

		temp = strchr(path + cursor, PATH_DELIM); //Find next delimiter

		if((temp == NULL) || (*(temp + 1) == 0))	//If not found (or terminating zero after it found) 
													//- it is the last file/directory in the path
		{
			end = (pathLngt - 1) - ((temp != NULL)?1:0); //Calculated index of the last character (ignore delimiter if exist)
			last = true;
		}
		else
			end = temp - path - 1 ; //Calculated index of the last character

		if((end - cursor + 1) > (FNAME_MAX_LNGT + FEXTENSION_MAX_LNGT + 1)) //Check if file/directory name is not to long
		{
			setLastError(ERR_BAD_ADDRESS);
			return 0;
		}

		temp = strnrchr(path + cursor, '.', end - cursor + 1);
		if(temp != NULL)
		{
			nameSize = (temp - path) - cursor;
			extensionSize = end - (temp - path);

			if((extensionSize > 3) || 
				(nameSize > 8)) //Check filename and 
												  //extension size (if exist)
			{
				setLastError(ERR_BAD_ADDRESS);
				return 0;
			}
		}
		else
		{
			extensionSize = 0;
			nameSize = end - cursor + 1;
		}
		
		//temp -> last dot or NULL

			//Set current folder to root directory
		
		//Get root directory first block
		//------Search-----//
		if(currentDir == 0)
		{
			if((resDirEntry = findEntry(rootDirCopy, partInfo->bootBlock.rootDirEntriesN, path + cursor, nameSize, temp + 1, extensionSize, ignoreVolLabel)) < 0)
			{
				setLastError(ERR_BAD_ADDRESS);
				return 0;
			}
			else if(last || isDir(rootDirCopy + resDirEntry*32))
			{
				found = true;
				inRoot = true;
				currentDir = (u16int)((rootDirCopy[resDirEntry * 32 + 0x1b] << 8) | (rootDirCopy[resDirEntry * 32 + 0x1a]));
			}
			else
			{
				setLastError(ERR_BAD_ADDRESS);
				return 0;
			}

		}
		else
		{
			while((currentDir != 0xFFFF) && !found)
			{
				partRead(partName, buff, clusterToSector(currentDir, &partInfo->bootBlock), partInfo->bootBlock.blocksPerAllocUnit);
				if(getLastError() != NO_ERROR)
				{
					return 0;
				}
				if((resDirEntry = findEntry(buff,
										partInfo->bootBlock.blocksPerAllocUnit * partInfo->bootBlock.bytesPerBlock / FILE_ENTRY_SIZE,
										path + cursor,
										nameSize,
										temp + 1,
										extensionSize,
										ignoreVolLabel)) >= 0)
				{
					if(last || isDir(buff + resDirEntry*32))
					{
						found = true;
						inRoot = false;
						currentDir = (u16int)((buff[resDirEntry * 32 + 0x1b] << 8) | (buff[resDirEntry * 32 + 0x1a]));
					}
				}
				else
					currentDir = fatCopy[currentDir];
			}
		}
		if(!found)
		{
			setLastError(ERR_BAD_ADDRESS);
			return 0;
		}

		cursor = end + 1;
		if(path[cursor] == PATH_DELIM)
			cursor++;
		rootDir = false;
	}

	//Fill result struct
	fillFileDescriptor((inRoot?rootDirCopy:buff) + (resDirEntry * 32), resFileInfo, partName);
	setLastError(NO_ERROR);
	kfree(buff);
	return currentDir;
}

/**
	Returns entry number. -1 if not found.
*/
s32int findEntry(u8int *buff,
				s32int numberOfEntries,
				s8int *name,
				s8int nameLngt,
				s8int *extension,
				s8int extLngt,
				BOOL ignoreVolLabel)
{
	BOOL found = false;
	s32int i = 0;
	found = false;
	for(i = 0; (i < numberOfEntries) && !found; i++)
	{
		if(nameCmp(buff + i * 32, (u8int *)name, nameLngt, FNAME_MAX_LNGT)) //Compare name
		{

			if(nameCmp(buff + i * 32 + FNAME_MAX_LNGT, (u8int *)extension, extLngt, FEXTENSION_MAX_LNGT)) //Compare extension
			{
				if(!ignoreVolLabel)
					found = true;
				else
					if(*(buff + i * 32 + 0x0b) & 0x08) //Check if entry is volume label
						found = false;
					else
						found = true;
			}
		}
	}
		
	//i -> found entry (if found)
	if(found)
		return --i;
	else
		return -1;
}

static BOOL nameCmp(u8int *dirEntry, u8int *dirName, u8int dirNameLngt, u8int dirEntryLngt) //
{
	u32int i = 0;
	BOOL equals = true;
	while(dirEntryLngt-- && equals)
	{
		if(dirNameLngt)
		{
			equals = *dirEntry++ == *dirName++;
			dirNameLngt--;
		}
		else //Check trailing spaces
		{
			equals = *dirEntry++ == ' ';
		}
	}
	return equals;
}

u32int clusterToSector(u32int cluster, BootBlock *bootBlock)
{
	//Clusters are counted starting from 2 (clusters - 2). (cluster == allocation unit).
	return bootBlock->rootDirStartSector + bootBlock->rootDirSize + (cluster - 2) * bootBlock->blocksPerAllocUnit;
}

BOOL isDir(u8int* entry)
{
	return (*(entry + 0x0b) & DIRECTORY)?true:false;
}

//entry pointer to a file entry of file name
BOOL isDeleted(u8int * entry)
{
	return *(entry) == DELETED;
}

BOOL testAttribute(u8int* entry, u8int test)
{
	return (*(entry + 0x0b) & test)?true:false;
}

void fillFileDescriptor(u8int* entry, FileDescriptor* file, s8int partition)
{
	u8int i = 0;

	file->isDirectory = isDir(entry);
	file->isHidden = testAttribute(entry, HIDDEN);
	file->isReadOnly = testAttribute(entry, READ_ONLY);
	file->isSystem = testAttribute(entry, SYSTEM);
	file->isVolLabel = testAttribute(entry, VOLUME_LABEL);

	for(i = 0; (i < 8) && (entry[i] != ' '); i++)
		file->name[i] = entry[i];

	file->name[i] = 0;

	if(!file->isDirectory)
	{
		for(i = 8; (i < 8 + 3) && (entry[i] != ' '); i++)
			file->extension[i - 8] = entry[i];

		file->extension[i - 8] = 0;
	}
	else
		file->extension[0] = 0;
	
	file->timeLastUpdated = (((entry[0x17] << 8) & 0xF0) | (entry[0x16] & 0x0F));
	file->dateLastUpdated = (((entry[0x19] << 8) & 0xF0) | (entry[0x18] & 0x0F));
	file->startingCluster = (u16int)((entry[0x1b] << 8) | (entry[0x1a]));
	file->size = (u32int)((entry[0x1f] << 24) | (entry[0x1e] << 16) | (entry[0x1d] << 8) | entry[0x1c]);
	file->partition = partition;
}

/**
	Copies a file content to a specified location. Returns number of bytes written.
	Assumes that buffer is large enough to fit a file.
*/
u32int loadFile(FileDescriptor* file, u8int* buff)
{
	u16int currentCluster = file->startingCluster;
	u32int currentSector = 0;
	u32int written = 0, blocksToRead = 0, reminder = 0;
	u32int bytesPerAllocUnit;
	u8int* blockBuff;;
	Partition* partInfo;
	blockBuff = (u8int*)kmalloc(512);
	setLastError(NO_ERROR);

	partInfo = getMountedPartitionInfo(file->partition); //Get information about partition where file is located
	if(getLastError() != NO_ERROR)
		return 0;

	if(file->isDirectory || file->startingCluster == 0)
	{
		setLastError(ERR_IS_DIR);
		return 0;
	}


	bytesPerAllocUnit = partInfo->bootBlock.blocksPerAllocUnit * partInfo->bootBlock.bytesPerBlock;

	while((written < file->size) && (currentCluster != 0xFFFF)) //Iterate through clusters and load to memory
	{
		currentSector = clusterToSector(currentCluster, &(partInfo->bootBlock));

		if((file->size - written) > bytesPerAllocUnit)
		{
			blocksToRead = partInfo->bootBlock.blocksPerAllocUnit;
			reminder = 0;
		}
		else
		{
			blocksToRead =(u32int)((file->size - written) / partInfo->bootBlock.bytesPerBlock);
			reminder = (u32int)((file->size - written) % partInfo->bootBlock.bytesPerBlock);
		}
		if(blocksToRead > 0)
		{
			written = written + partRead(file->partition, buff + written, currentSector, blocksToRead) * partInfo->bootBlock.bytesPerBlock;
			if(getLastError() != NO_ERROR)
				return written;
		}

		if(reminder != 0)
		{
			currentSector +=  blocksToRead;
			partRead(file->partition, blockBuff, currentSector, 1);
			if(getLastError() != NO_ERROR)
				return written;
			memcpy(buff + written, blockBuff, reminder);
			written += reminder;
		}

		currentCluster = fatCopy[currentCluster];
	}
	kfree(blockBuff);
	return written;
}
